﻿//-----------------------------------------------------------------------
// <copyright file="DistributedPubSubSettings.cs" company="Akka.NET Project">
//     Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
//     Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using Akka.Actor;
using Akka.Configuration;
using Akka.Routing;

namespace Akka.Cluster.Tools.PublishSubscribe
{
    /// <summary>
    /// TBD
    /// </summary>
    public sealed record DistributedPubSubSettings : INoSerializationVerificationNeeded
    {
        /// <summary>
        /// Creates cluster publish/subscribe settings from the default configuration `akka.cluster.pub-sub`.
        /// </summary>
        /// <param name="system">TBD</param>
        /// <exception cref="ArgumentException">TBD</exception>
        /// <returns>TBD</returns>
        public static DistributedPubSubSettings Create(ActorSystem system)
        {
            system.Settings.InjectTopLevelFallback(DistributedPubSub.DefaultConfig());

            var config = system.Settings.Config.GetConfig("akka.cluster.pub-sub");
            if (config.IsNullOrEmpty())
                throw ConfigurationException.NullOrEmptyConfig<DistributedPubSubSettings>("akka.cluster.pub-sub");

            return Create(config);
        }

        /// <summary>
        /// Creates cluster publish subscribe settings from provided configuration with the same layout as `akka.cluster.pub-sub`.
        /// </summary>
        /// <param name="config">TBD</param>
        /// <exception cref="ArgumentException">TBD</exception>
        /// <returns>TBD</returns>
        public static DistributedPubSubSettings Create(Config config)
        {
            if (config.IsNullOrEmpty())
                throw ConfigurationException.NullOrEmptyConfig<DistributedPubSubSettings>();

            var routingLogic = config.GetString("routing-logic")?.ToLowerInvariant() switch
            {
                "random" => (RoutingLogic) new RandomLogic(),
                "round-robin" => new RoundRobinRoutingLogic(),
                "broadcast" => new BroadcastRoutingLogic(),
                "consistent-hashing" => throw new ArgumentException("Consistent hashing routing logic cannot be used by the pub-sub mediator"),
                var unknown => throw new ArgumentException($"Unknown routing logic is tried to be applied to the pub-sub mediator: {unknown}")
            };

            // TODO: This will fail if DistributedPubSub.DefaultConfig() is not inside the fallback chain.
            // TODO: "gossip-interval" key depends on Config.GetTimeSpan() to return a TimeSpan.Zero default.
            // TODO: "removed-time-to-live" key depends on Config.GetTimeSpan() to return a TimeSpan.Zero default.
            // TODO: "max-delta-elements" key depends on Config.GetInt() to return a 0 default.
            return new DistributedPubSubSettings(
                config.GetString("role", ""),
                routingLogic,
                config.GetTimeSpan("gossip-interval"),
                config.GetTimeSpan("removed-time-to-live"),
                config.GetInt("max-delta-elements"),
                config.GetBoolean("send-to-dead-letters-when-no-subscribers"),
                config.GetInt("buffered-messages.max-per-topic"),
                config.GetTimeSpan("buffered-messages.timeout-check-interval"));
        }

        /// <summary>
        /// The mediator starts on members tagged with this role. Uses all if undefined.
        /// </summary>
        public string Role { get; private init; }

        /// <summary>
        /// The routing logic to use for <see cref="DistributedPubSubMediator"/>.
        /// </summary>
        public RoutingLogic RoutingLogic { get; private init; }

        /// <summary>
        /// How often the <see cref="DistributedPubSubMediator"/> should send out gossip information
        /// </summary>
        public TimeSpan GossipInterval { get; private init; }

        /// <summary>
        /// Removed entries are pruned after this duration.
        /// </summary>
        public TimeSpan RemovedTimeToLive { get; private init; }

        /// <summary>
        /// Maximum number of elements to transfer in one message when synchronizing the registries.
        /// Next chunk will be transferred in next round of gossip.
        /// </summary>
        public int MaxDeltaElements { get; private init; }

        /// <summary>
        /// When a message is published to a topic with no subscribers send it to the dead letters.
        /// </summary>
        public bool SendToDeadLettersWhenNoSubscribers { get; private init; }
        
        /// <summary>
        /// The maximum <see cref="PublishWithAck"/> message buffer size for each topic 
        /// </summary>
        public int MaxBufferedMessagePerTopic { get; private init; }
        
        /// <summary>
        /// Determine the interval on which all buffered <see cref="PublishWithAck"/> message will be checked for timeout condition
        /// </summary>
        public TimeSpan BufferedMessageTimeoutCheckInterval { get; private init; }
        
        /// <summary>
        /// Creates a new instance of the <see cref="DistributedPubSubSettings" />.
        /// </summary>
        /// <param name="role">The role that will host <see cref="DistributedPubSubMediator"/> instances.</param>
        /// <param name="routingLogic">Optional. The routing logic used for distributing messages for topic groups.</param>
        /// <param name="gossipInterval">The gossip interval for propagating topic/subscriber data to other mediators.</param>
        /// <param name="removedTimeToLive">The amount of time it takes to prune a deactivated subscriber from the network.</param>
        /// <param name="maxDeltaElements">The maximum number of delta elements that can be propagated in a single gossip tick.</param>
        /// <param name="sendToDeadLettersWhenNoSubscribers">When a message is published to a topic with no subscribers send it to the dead letters.</param>
        /// <exception cref="ArgumentException">Thrown if a user tries to use a <see cref="ConsistentHashingRoutingLogic"/> with routingLogic.</exception>
        [Obsolete("Use .ctor that supports WaitForSubscribers instead. Since 1.4.42")]
        public DistributedPubSubSettings(
            string role,
            RoutingLogic routingLogic,
            TimeSpan gossipInterval,
            TimeSpan removedTimeToLive,
            int maxDeltaElements,
            bool sendToDeadLettersWhenNoSubscribers)
            : this(
                role: role,
                routingLogic: routingLogic,
                gossipInterval: gossipInterval,
                removedTimeToLive: removedTimeToLive,
                maxDeltaElements: maxDeltaElements, 
                sendToDeadLettersWhenNoSubscribers: sendToDeadLettersWhenNoSubscribers,
                maxBufferedMessagePerTopic: 0,
                bufferedMessageTimeoutCheckInterval: TimeSpan.Zero)
        {
        }

        /// <summary>
        /// Creates a new instance of the <see cref="DistributedPubSubSettings" />.
        /// </summary>
        /// <param name="role">The role that will host <see cref="DistributedPubSubMediator"/> instances.</param>
        /// <param name="routingLogic">Optional. The routing logic used for distributing messages for topic groups.</param>
        /// <param name="gossipInterval">The gossip interval for propagating topic/subscriber data to other mediators.</param>
        /// <param name="removedTimeToLive">The amount of time it takes to prune a deactivated subscriber from the network.</param>
        /// <param name="maxDeltaElements">The maximum number of delta elements that can be propagated in a single gossip tick.</param>
        /// <param name="sendToDeadLettersWhenNoSubscribers">When a message is published to a topic with no subscribers send it to the dead letters.</param>
        /// <param name="maxBufferedMessagePerTopic">Maximum message buffer size for each topic</param>
        /// <param name="bufferedMessageTimeoutCheckInterval">Buffered message timeout condition check interval</param>
        /// <exception cref="ArgumentException">Thrown if a user tries to use a <see cref="ConsistentHashingRoutingLogic"/> with routingLogic.</exception>
        public DistributedPubSubSettings(
            string role,
            RoutingLogic routingLogic,
            TimeSpan gossipInterval,
            TimeSpan removedTimeToLive,
            int maxDeltaElements,
            bool sendToDeadLettersWhenNoSubscribers,
            int maxBufferedMessagePerTopic,
            TimeSpan bufferedMessageTimeoutCheckInterval)
        {
            if (routingLogic is ConsistentHashingRoutingLogic)
            {
                throw new ArgumentException("ConsistentHashingRoutingLogic cannot be used by the pub-sub mediator");
            }

            Role = !string.IsNullOrEmpty(role) ? role : null;
            RoutingLogic = routingLogic;
            GossipInterval = gossipInterval;
            RemovedTimeToLive = removedTimeToLive;
            MaxDeltaElements = maxDeltaElements;
            SendToDeadLettersWhenNoSubscribers = sendToDeadLettersWhenNoSubscribers;
            MaxBufferedMessagePerTopic = maxBufferedMessagePerTopic;
            BufferedMessageTimeoutCheckInterval = bufferedMessageTimeoutCheckInterval;
        }

        public DistributedPubSubSettings WithRole(string role)
            => this with { Role = role };

        public DistributedPubSubSettings WithRoutingLogic(RoutingLogic routingLogic)
        {
            if (routingLogic is ConsistentHashingRoutingLogic)
                throw new ArgumentException("Consistent hashing routing logic cannot be used by the pub-sub mediator");
            
            return this with { RoutingLogic = routingLogic };
        }

        public DistributedPubSubSettings WithGossipInterval(TimeSpan gossipInterval)
            => this with { GossipInterval = gossipInterval };

        public DistributedPubSubSettings WithRemovedTimeToLive(TimeSpan removedTtl)
            => this with { RemovedTimeToLive = removedTtl };

        public DistributedPubSubSettings WithMaxDeltaElements(int maxDeltaElements)
            => this with { MaxDeltaElements = maxDeltaElements };

        public DistributedPubSubSettings WithSendToDeadLettersWhenNoSubscribers(bool sendToDeadLetterWhenNoSubscribers)
            => this with { SendToDeadLettersWhenNoSubscribers = sendToDeadLetterWhenNoSubscribers }; 
        
        public DistributedPubSubSettings WithMaxBufferedMessagePerTopic(int maxBufferedMessagePerTopic)
            => this with { MaxBufferedMessagePerTopic = maxBufferedMessagePerTopic };
        
        public DistributedPubSubSettings WithBufferedMessageTimeoutCheckInterval(TimeSpan bufferedMessageTimeoutCheckInterval)
            => this with { BufferedMessageTimeoutCheckInterval = bufferedMessageTimeoutCheckInterval };
        
    }
}
